﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CoreEx.Common.Extensions;
using CoreEx.Database.Metadata;
using CoreEx.Database.Metadata.Generation;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;
using System.CodeDom;
using ITypeGenerator = CoreEx.Database.Metadata.Generation.ITypeGenerator;

namespace CoreEx.Database.MetaData.Implementation.Generation
{
    [Implements(typeof(ITypeGenerator))]
    public class TypeGenerator : ITypeGenerator, IInitialize
    {

        private CodeTypeDeclaration _codeTypeDeclaration;

        public TypeGenerator()
        {
            NamespaceReferences = new List<string>();            
        }


        public IFieldNameFormatter FieldNameFormatter { get; set; }


        /// <summary>
        /// Gets or sets the <see cref="ILinkTableInspector"/> instance that is responsible 
        /// for determinig if a given <see cref="TableInfo"/> represents a link table.
        /// </summary>
        public ILinkTableInspector LinkTableInspector { get; set; }

        public CodeTypeDeclaration Generate(TableInfo tableInfo)
        {                        
            _codeTypeDeclaration = new CodeTypeDeclaration();
            _codeTypeDeclaration.Name = GetTypeName(tableInfo);

            
            ProcessNoneRelationalProperties(tableInfo);
            ProcessOneToManyRelationships(tableInfo);
            ProcessManyToOneRelationships(tableInfo);
            ProcessManyToManyRelationships(tableInfo);
            ProcessOneToOneRelationships(tableInfo);
            return _codeTypeDeclaration;
        }

        private void ProcessManyToManyRelationships(TableInfo tableInfo)
        {
            IEnumerable<ForeignKeyInfo> foreignkeys = GetForeignKeysThatReferencesThis(tableInfo);
            var linkTables = foreignkeys.Select(fki => fki.Table).Where(IsLinktable);
            foreach (var linkTable in linkTables)
            {
                var targetTable = linkTable.ForeignKeys.Where(fki => fki.PrimaryTable != tableInfo).First().PrimaryTable;
                string typeName = GetTypeName(targetTable);
                string propertyName = GetNavigationPropertyName(typeName, CardinalityType.ManyToMany);
                

                var propertyDescription = new PropertyDescription
                                              {
                                                  Cardinality = CardinalityType.ManyToMany,
                                                  ForeignKey = linkTable.ForeignKeys.Where(fki => fki.PrimaryTable == tableInfo).First(),
                                                  Name = propertyName               
                                              };
                
                var codeMemberProperty = new CodeMemberProperty();
                codeMemberProperty.Type =
                    new CodeTypeReference("System.Collections.Generic.IList",
                                          new CodeTypeReference(GetTypeName(targetTable)));

                codeMemberProperty.Name = propertyName;
                codeMemberProperty.UserData.Add("PropertyDescription",propertyDescription);


                _codeTypeDeclaration.Members.Add(codeMemberProperty);

                var codeMemberField = CreateBackingField(codeMemberProperty);
                CodeMethodReturnStatement returnStatement = new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), codeMemberField.Name));
                codeMemberProperty.GetStatements.Add(returnStatement);
                
            }
        }


        private bool IsLinktable(TableInfo tableInfo)
        {
            return LinkTableInspector.IsLinkTable(tableInfo);
        }



        private void ProcessOneToOneRelationships(TableInfo tableInfo)
        {
            //Find all tables that references this table where the foreignkey is also the primary key.

            var foreignkeyCandidates = GetForeignKeysThatReferencesThis(tableInfo);
            var foreignkeys =  foreignkeyCandidates.Where(ForeignKeyIsAlsoThePrimaryKey);
            foreach (var foreignKeyInfo in foreignkeys)
            {
                string typeName = GetTypeName(foreignKeyInfo.Table); 
                
                var propertyDescription = new PropertyDescription
                                              {
                                                  Cardinality = CardinalityType.OneToOne,
                                                  ForeignKey = foreignKeyInfo,
                                                  Name = typeName,
                                                  TypeName = typeName
            
                                              };
                var codeMemberProperty = new CodeMemberProperty();
                codeMemberProperty.Name = typeName;
                codeMemberProperty.Type = new CodeTypeReference(typeName);
                codeMemberProperty.UserData.Add("PropertyDescription",propertyDescription);
                _codeTypeDeclaration.Members.Add(codeMemberProperty);
            }

        }

        private void ProcessManyToOneRelationships(TableInfo tableInfo)
        {
            string propertyName= null;
                       
            var foreignkeys = tableInfo.ForeignKeys;
            foreach (var foreignKeyInfo in foreignkeys)
            {
                string primaryTableTypeName = GetTypeName(foreignKeyInfo.PrimaryTable);
                if (foreignKeyInfo.Columns.Count == 1)
                {
                    var foreignKeyColumnInfo = foreignKeyInfo.Columns.First();
                    var foreignKeyColumnName = foreignKeyColumnInfo.ForeignColumnInfo.ColumnName;
                    var primaryKeyColumnName = foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName;

                    if (foreignKeyColumnName != primaryKeyColumnName)
                    {
                        if (foreignKeyColumnName.Contains(primaryKeyColumnName))
                            propertyName = foreignKeyColumnName.Replace(primaryKeyColumnName, primaryTableTypeName);
                        else
                            propertyName = primaryTableTypeName;        
                    }
                    else
                        propertyName = primaryTableTypeName;        
                }
                else
                {
                    propertyName = primaryTableTypeName;
                    if (foreignKeyInfo.Table == foreignKeyInfo.PrimaryTable)
                        propertyName = string.Format("Parent{0}", propertyName);
                }

                var propertyDescription = new PropertyDescription()
                                              {
                                                  Cardinality = CardinalityType.ManyToOne,
                                                  ForeignKey = foreignKeyInfo,
                                                  Name = propertyName,
                                                  TypeName = primaryTableTypeName
                                              };

                var codeMemberProperty = new CodeMemberProperty();
                codeMemberProperty.Name = propertyName;
                codeMemberProperty.Type = new CodeTypeReference(primaryTableTypeName);

                codeMemberProperty.UserData.Add("PropertyDescription", propertyDescription);
                _codeTypeDeclaration.Members.Add(codeMemberProperty);
            }
        }

        private void ProcessNoneRelationalProperties(TableInfo tableInfo)
        {
            var candidateColumns = tableInfo.Columns.Values;
            var nonrelationalColumns = candidateColumns.Except(ColumnsThatParticipateInForeignKeys(tableInfo));
            foreach (var nonrelationalColumn in nonrelationalColumns)
            {
                var propertyName = nonrelationalColumn.ColumnName;
                var codeMemberProperty = new CodeMemberProperty()
                                             {
                                                 Name = propertyName,
                                                 Type = new CodeTypeReference(nonrelationalColumn.DataType)
                                             };

                var propertyDescription = new PropertyDescription()
                                              {
                                                  Cardinality = CardinalityType.ManyToOne,                                                                                                    
                                              };

                codeMemberProperty.UserData.Add("PropertyDescription", propertyDescription);

                _codeTypeDeclaration.Members.Add(codeMemberProperty);
            }                        
        }





        private static IEnumerable<ColumnInfo> ColumnsThatParticipateInForeignKeys(TableInfo tableInfo)
        {
            return tableInfo.ForeignKeys.SelectMany(fki => fki.Columns).Select(fkci => fkci.ForeignColumnInfo);
        }


        private static string GetTableName(TableInfo tableInfo)
        {
            return tableInfo.TableName.Replace(" ", "");
        }


        private string GetTypeName(TableInfo tableInfo)
        {
            return GetTableName(tableInfo).ToSingular();
        }


        


        
        
        

        

        public IList<string> NamespaceReferences { get; private set; }
        


        private void ProcessOneToManyRelationships(TableInfo tableInfo)
        {                                    
            //Find the foreign keys in the foreign tables that references this table
            IEnumerable<ForeignKeyInfo> foreignTableForeignKeyCandidates =
                GetForeignKeysThatReferencesThis(tableInfo);

            if (SomeForeignTableHasMultipleReferencesToThisTable(foreignTableForeignKeyCandidates))
                return;

            //Exclude the foreign keys that is also the primary key in the foreign table
            //In that case it represents an "one to one" relationship.
            IEnumerable<ForeignKeyInfo> foreignTableForeignKeys =
                foreignTableForeignKeyCandidates.Where(fk => !ForeignKeyIsAlsoThePrimaryKey(fk) && !IsLinktable(fk.Table));

            foreach (ForeignKeyInfo foreignKeyInfo in foreignTableForeignKeys)
            {
                var classMember = new PropertyDescription();
                classMember.Cardinality = CardinalityType.OneToMany;
                classMember.TypeName = GetTypeName(foreignKeyInfo.Table);                
                classMember.ForeignKey = foreignKeyInfo;
                classMember.Name = GetNavigationPropertyName(classMember.TypeName, classMember.Cardinality);
                

                CodeMemberProperty codeMemberProperty = new CodeMemberProperty();
                codeMemberProperty.Type =
                    new CodeTypeReference("System.Collections.Generic.IList",
                                          new CodeTypeReference(GetTypeName(foreignKeyInfo.Table)));
                codeMemberProperty.Name = GetNavigationPropertyName(GetTypeName(foreignKeyInfo.Table), CardinalityType.OneToMany);

                var codeMemberField = CreateBackingField(codeMemberProperty);

                CodeMethodReturnStatement returnStatement = new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), codeMemberField.Name));
                codeMemberProperty.GetStatements.Add(returnStatement);
                codeMemberProperty.UserData.Add("PropertyDescription",classMember);
                _codeTypeDeclaration.Members.Add(codeMemberProperty);
            }
            
        }


        private CodeMemberField CreateBackingField(CodeMemberProperty codeMemberProperty)
        {
            var codeMemberField = new CodeMemberField();
            codeMemberField.Type = codeMemberProperty.Type;
            codeMemberField.Name = FieldNameFormatter.GetFieldName(codeMemberProperty.Name);
            codeMemberField.InitExpression = new CodeObjectCreateExpression(new CodeTypeReference("System.Collections.Generic.List",
                                                                            codeMemberField.Type.TypeArguments[0]));

            _codeTypeDeclaration.Members.Add(codeMemberField);

            return codeMemberField;

        }



        private string GetNavigationPropertyName(string typeName, CardinalityType cardinality)
        {
            if (cardinality == CardinalityType.ManyToMany || cardinality == CardinalityType.OneToMany)
                return typeName.ToPlural();
            return typeName;
        }

        

        private static bool SomeForeignTableHasMultipleReferencesToThisTable(IEnumerable<ForeignKeyInfo> foreignTableForeignKeyCandidates)
        {
            return foreignTableForeignKeyCandidates.GroupBy(fki => fki.Table).Where(g => g.Count() > 1).Count() > 0;
        }


        private static bool ForeignKeyIsAlsoThePrimaryKey(ForeignKeyInfo foreignKeyInfo)
        {
            var primaryKeyColumns = foreignKeyInfo.Columns.Select(fkc => fkc.ForeignColumnInfo);
            var foreignKeyColumns = foreignKeyInfo.Table.PrimaryKey.Columns;
            return foreignKeyColumns.Except(primaryKeyColumns).Count() == 0;

        }

        private static IEnumerable<ForeignKeyInfo> GetForeignKeysThatReferencesThis(TableInfo tableInfo)
        {
            return tableInfo.Relations.Select(r => r.ForeignKey).Except(tableInfo.ForeignKeys);
        }

        public virtual void Initialize(IServiceContainer source)
        {
            FieldNameFormatter = source.GetService<IFieldNameFormatter>();
            LinkTableInspector = source.GetService<ILinkTableInspector>();
        }
    }
}
